/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.response;

import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.DocList;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.DocIterator;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.ArrayList;

/**
 * 
 * 
 * This class serves as a basis from which {@link QueryResponseWriter}s can be
 * developed. The class provides a single method
 * {@link #write(SingleResponseWriter, SolrQueryRequest, SolrQueryResponse)}
 * that allows users to implement a {@link SingleResponseWriter} sub-class which
 * defines how to output {@link SolrInputDocument}s or a
 * {@link SolrDocumentList}.
 * 
 * @version $Id: BaseResponseWriter.java 1098906 2011-05-03 05:02:02Z ryan $
 * @since 1.5
 * 
 * @deprecated use {@link TextResponseWriter} or {@link BinaryResponseWriter}
 *             see SOLR-2485
 */
public abstract class BaseResponseWriter {

	private static final Logger LOG = LoggerFactory
			.getLogger(BaseResponseWriter.class);

	private static final String SCORE_FIELD = "score";

	/**
	 * 
	 * The main method that allows users to write {@link SingleResponseWriter}s
	 * and provide them as the initial parameter <code>responseWriter</code> to
	 * this method which defines how output should be generated.
	 * 
	 * @param responseWriter
	 *            The user-provided {@link SingleResponseWriter} implementation.
	 * @param request
	 *            The provided {@link SolrQueryRequest}.
	 * @param response
	 *            The provided {@link SolrQueryResponse}.
	 * @throws IOException
	 *             If any error occurs.
	 */
	public void write(SingleResponseWriter responseWriter,
			SolrQueryRequest request, SolrQueryResponse response)
			throws IOException {
		responseWriter.start();
		NamedList nl = response.getValues();
		for (int i = 0; i < nl.size(); i++) {
			String name = nl.getName(i);
			Object val = nl.getVal(i);
			if ("responseHeader".equals(name)) {
				Boolean omitHeader = request.getParams().getBool(
						CommonParams.OMIT_HEADER);
				if (omitHeader == null || !omitHeader)
					responseWriter.writeResponseHeader((NamedList) val);
			} else if (val instanceof SolrDocumentList) {
				SolrDocumentList list = (SolrDocumentList) val;
				DocListInfo info = new DocListInfo((int) list.getNumFound(),
						list.size(), (int) list.getStart(), list.getMaxScore());
				if (responseWriter.isStreamingDocs()) {
					responseWriter.startDocumentList(name, info);
					for (SolrDocument solrDocument : list)
						responseWriter.writeDoc(solrDocument);
					responseWriter.endDocumentList();
				} else {
					responseWriter.writeAllDocs(info, list);
				}
			} else if (val instanceof DocList) {
				DocList docList = (DocList) val;
				int sz = docList.size();
				IdxInfo idxInfo = new IdxInfo(request.getSchema(),
						request.getSearcher(), response.getReturnFields());
				DocListInfo info = new DocListInfo(docList.matches(),
						docList.size(), docList.offset(), docList.maxScore());
				DocIterator iterator = docList.iterator();
				if (responseWriter.isStreamingDocs()) {
					responseWriter.startDocumentList(name, info);
					for (int j = 0; j < sz; j++) {
						SolrDocument sdoc = getDoc(iterator.nextDoc(), idxInfo);
						if (idxInfo.includeScore && docList.hasScores()) {
							sdoc.addField(SCORE_FIELD, iterator.score());
						}
						responseWriter.writeDoc(sdoc);
					}
				} else {
					ArrayList<SolrDocument> list = new ArrayList<SolrDocument>(
							docList.size());
					for (int j = 0; j < sz; j++) {
						SolrDocument sdoc = getDoc(iterator.nextDoc(), idxInfo);
						if (idxInfo.includeScore && docList.hasScores()) {
							sdoc.addField(SCORE_FIELD, iterator.score());
						}
						list.add(sdoc);
					}
					responseWriter.writeAllDocs(info, list);
				}

			} else {
				responseWriter.writeOther(name, val);

			}
		}
		responseWriter.end();

	}

	/**
	 * No ops implementation so that the implementing classes do not have to do
	 * it
	 */
	public void init(NamedList args) {
	}

	private static class IdxInfo {
		IndexSchema schema;
		SolrIndexSearcher searcher;
		Set<String> returnFields;
		boolean includeScore;

		private IdxInfo(IndexSchema schema, SolrIndexSearcher searcher,
				Set<String> returnFields) {
			this.schema = schema;
			this.searcher = searcher;
			this.includeScore = returnFields != null
					&& returnFields.contains(SCORE_FIELD);
			if (returnFields != null) {
				if (returnFields.size() == 0
						|| (returnFields.size() == 1 && includeScore)
						|| returnFields.contains("*")) {
					returnFields = null; // null means return all stored fields
				}
			}
			this.returnFields = returnFields;

		}
	}

	private static SolrDocument getDoc(int id, IdxInfo info) throws IOException {
		Document doc = info.searcher.doc(id);
		SolrDocument solrDoc = new SolrDocument();
		for (Fieldable f : doc.getFields()) {
			String fieldName = f.name();
			if (info.returnFields != null
					&& !info.returnFields.contains(fieldName))
				continue;
			SchemaField sf = info.schema.getFieldOrNull(fieldName);
			FieldType ft = null;
			if (sf != null)
				ft = sf.getType();
			Object val = null;
			if (ft == null) { // handle fields not in the schema
				if (f.isBinary())
					val = f.getBinaryValue();
				else
					val = f.stringValue();
			} else {
				try {
					if (BinaryResponseWriter.KNOWN_TYPES
							.contains(ft.getClass())) {
						val = ft.toObject(f);
					} else {
						val = ft.toExternal(f);
					}
				} catch (Exception e) {
					// There is a chance of the underlying field not really
					// matching the
					// actual field type . So ,it can throw exception
					LOG.warn(
							"Error reading a field from document : " + solrDoc,
							e);
					// if it happens log it and continue
					continue;
				}
			}
			if (sf != null && sf.multiValued()
					&& !solrDoc.containsKey(fieldName)) {
				ArrayList l = new ArrayList();
				l.add(val);
				solrDoc.addField(fieldName, l);
			} else {
				solrDoc.addField(fieldName, val);
			}
		}

		return solrDoc;
	}

	public static class DocListInfo {
		public final int numFound;
		public final int start;
		public Float maxScore = null;
		public final int size;

		public DocListInfo(int numFound, int sz, int start, Float maxScore) {
			this.numFound = numFound;
			size = sz;
			this.start = start;
			this.maxScore = maxScore;
		}
	}

	/**
	 * 
	 * Users wanting to define custom {@link QueryResponseWriter}s that deal
	 * with {@link SolrInputDocument}s and {@link SolrDocumentList} should
	 * override the methods for this class. All the methods are w/o body because
	 * the user is left to choose which all methods are required for his purpose
	 */
	public static abstract class SingleResponseWriter {

		/**
		 * This method is called at the start of the {@link QueryResponseWriter}
		 * output. Override this method if you want to provide a header for your
		 * output, e.g., XML headers, etc.
		 * 
		 * @throws IOException
		 *             if any error occurs.
		 */
		public void start() throws IOException {
		}

		/**
		 * This method is called at the start of processing a
		 * {@link SolrDocumentList}. Those that override this method are
		 * provided with {@link DocListInfo} object to use to inspect the output
		 * {@link SolrDocumentList}.
		 * 
		 * @param info
		 *            Information about the {@link SolrDocumentList} to output.
		 */
		public void startDocumentList(String name, DocListInfo info)
				throws IOException {
		}

		/**
		 * This method writes out a {@link SolrDocument}, on a doc-by-doc basis.
		 * This method is only called when {@link #isStreamingDocs()} returns
		 * true.
		 * 
		 * @param solrDocument
		 *            The doc-by-doc {@link SolrDocument} to transform into
		 *            output as part of this {@link QueryResponseWriter}.
		 */
		public void writeDoc(SolrDocument solrDocument) throws IOException {
		}

		/**
		 * This method is called at the end of outputting a
		 * {@link SolrDocumentList} or on a doc-by-doc {@link SolrDocument}
		 * basis.
		 */
		public void endDocumentList() throws IOException {
		}

		/**
		 * This method defines how to output the {@link SolrQueryResponse}
		 * header which is provided as a {@link NamedList} parameter.
		 * 
		 * @param responseHeader
		 *            The response header to output.
		 */
		public void writeResponseHeader(NamedList responseHeader)
				throws IOException {
		}

		/**
		 * This method is called at the end of the {@link QueryResponseWriter}
		 * lifecycle. Implement this method to add a footer to your output,
		 * e.g., in the case of XML, the outer tag for your tag set, etc.
		 * 
		 * @throws IOException
		 *             If any error occurs.
		 */
		public void end() throws IOException {
		}

		/**
		 * Define this method to control how output is written by this
		 * {@link QueryResponseWriter} if the output is not a
		 * {@link SolrInputDocument} or a {@link SolrDocumentList}.
		 * 
		 * @param name
		 *            The name of the object to output.
		 * @param other
		 *            The object to output.
		 * @throws IOException
		 *             If any error occurs.
		 */
		public void writeOther(String name, Object other) throws IOException {
		}

		/**
		 * Overriding this method to return false forces all
		 * {@link SolrInputDocument}s to be spit out as a
		 * {@link SolrDocumentList} so they can be processed as a whole, rather
		 * than on a doc-by-doc basis. If set to false, this method calls
		 * {@link #writeAllDocs(org.apache.solr.response.BaseResponseWriter.DocListInfo, List)}
		 * , else if set to true, then this method forces calling
		 * {@link #writeDoc(SolrDocument)} on a doc-by-doc basis. one
		 * 
		 * @return True to force {@link #writeDoc(SolrDocument)} to be called,
		 *         False to force
		 *         {@link #writeAllDocs(org.apache.solr.response.BaseResponseWriter.DocListInfo, List)}
		 *         to be called.
		 */
		public boolean isStreamingDocs() {
			return true;
		}

		/**
		 * Writes out all {@link SolrInputDocument}s . This is invoked only if
		 * {@link #isStreamingDocs()} returns false.
		 * 
		 * @param info
		 *            Information about the {@link List} of {@link SolrDocument}
		 *            s to output.
		 * @param allDocs
		 *            A {@link List} of {@link SolrDocument}s to output.
		 * @throws IOException
		 *             If any error occurs.
		 */
		public void writeAllDocs(DocListInfo info, List<SolrDocument> allDocs)
				throws IOException {
		}

	}

}
